<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Sing Ruler实验 - 钢尺振动模拟</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: 'Microsoft YaHei', Arial, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
            color: #333;
        }
        .container {
            max-width: 1200px;
            margin: 0 auto;
            background: white;
            border-radius: 15px;
            box-shadow: 0 10px 30px rgba(0,0,0,0.2);
            overflow: hidden;
        }
        .header {
            background: linear-gradient(135deg, #9b59b6, #8e44ad);
            color: white;
            padding: 20px;
            text-align: center;
        }
        .header h1 {
            font-size: 1.8em;
            margin-bottom: 5px;
        }
        .content {
            padding: 20px;
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
        }
        .simulation-section {
            grid-column: 1 / -1;
        }
        .control-panel {
            background: #f8f9fa;
            padding: 20px;
            border-radius: 10px;
            border-left: 4px solid #3498db;
        }
        .info-panel {
            background: #f8f9fa;
            padding: 20px;
            border-radius: 10px;
            border-left: 4px solid #e74c3c;
        }
        .slider-group {
            margin: 15px 0;
        }
        label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
            color: #2c3e50;
        }
        input[type="range"] {
            width: 100%;
            margin: 5px 0;
        }
        .value-display {
            color: #e74c3c;
            font-weight: bold;
        }
        .btn {
            padding: 10px 15px;
            margin: 5px;
            background: linear-gradient(135deg, #3498db, #2980b9);
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 0.9em;
        }
        .btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 3px 10px rgba(0,0,0,0.2);
        }
        .btn-reset {
            background: linear-gradient(135deg, #e74c3c, #c0392b);
        }
        .btn-impulse {
            background: linear-gradient(135deg, #f39c12, #e67e22);
        }
        #simulationContainer {
            width: 100%;
            height: 400px;
            background: #1a1a1a;
            border-radius: 8px;
            margin: 15px 0;
        }
        .chart-container {
            background: white;
            padding: 15px;
            border-radius: 8px;
            margin: 10px 0;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        .btn-back {
            display: block;
            width: 200px;
            margin: 20px auto;
            padding: 12px;
            background: linear-gradient(135deg, #95a5a6, #7f8c8d);
            color: white;
            text-align: center;
            text-decoration: none;
            border-radius: 8px;
        }
        .real-time-data {
            background: #2c3e50;
            color: white;
            padding: 15px;
            border-radius: 8px;
            margin: 10px 0;
            font-family: monospace;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>🎵 Sing Ruler 实验 - 钢尺振动模拟</h1>
            <p>观察钢尺在桌面边缘的振动现象，探索物理参数对振动频率的影响</p>
        </div>
        
        <div class="content">
            <!-- 控制面板 -->
            <div class="control-panel">
                <h3>📊 物理参数控制</h3>
                
                <div class="slider-group">
                    <label>伸出长度: <span id="overhangValue" class="value-display">150 mm</span></label>
                    <input type="range" id="overhangSlider" min="50" max="250" value="150" step="5">
                </div>
                
                <div class="slider-group">
                    <label>杨氏模量: <span id="youngsValue" class="value-display">200 GPa</span></label>
                    <input type="range" id="youngsSlider" min="50" max="300" value="200" step="10">
                </div>
                
                <div class="slider-group">
                    <label>厚度: <span id="thicknessValue" class="value-display">1.00 mm</span></label>
                    <input type="range" id="thicknessSlider" min="50" max="200" value="100" step="5">
                </div>
                
                <div class="slider-group">
                    <label>阻尼系数: <span id="dampingValue" class="value-display">0.020</span></label>
                    <input type="range" id="dampingSlider" min="5" max="100" value="20" step="5">
                </div>
                
                <div style="margin-top: 20px;">
                    <button class="btn btn-impulse" onclick="applyImpulse()">💥 施加初始扰动</button>
                    <button class="btn btn-reset" onclick="resetSimulation()">🔄 重新开始</button>
                </div>
            </div>
            
            <!-- 信息面板 -->
            <div class="info-panel">
                <h3>📈 实时数据</h3>
                <div class="real-time-data" id="realTimeData">
                    时间: 0.0s<br>
                    角度: 0.0°<br>
                    振幅: 0.0mm<br>
                    频率: 0.0Hz<br>
                    状态: 准备中...
                </div>
                
                <h4>实验说明</h4>
                <p>这个模拟基于悬臂梁振动理论，展示了钢尺在桌面边缘的振动行为。调整参数观察振动频率和模式的变化。</p>
                
                <h4>物理原理</h4>
                <ul>
                    <li>悬臂梁弯曲振动方程</li>
                    <li>转动惯量与角动量守恒</li>
                    <li>阻尼振动与能量耗散</li>
                </ul>
            </div>
            
            <!-- 3D模拟区域 -->
            <div class="simulation-section">
                <h3>🎯 3D 振动模拟</h3>
                <div id="simulationContainer"></div>
            </div>
            
            <!-- 波形图 -->
            <div class="chart-container">
                <h3>📉 实时波形图</h3>
                <canvas id="waveChart" width="400" height="200"></canvas>
            </div>
            
            <!-- 频谱分析 -->
            <div class="chart-container">
                <h3>🎵 声波频谱分析</h3>
                <canvas id="soundChart" width="400" height="200"></canvas>
            </div>
        </div>
        
        <a href="catalog.html" class="btn-back">↩️ 返回实验目录</a>
    </div>

    <script>
// ==================== 物理模拟核心引擎 ====================
class SingRulerSimulation {
    constructor() {
        // 物理参数（与您VPython代码完全一致）
        this.YOUNGS_MODULUS = 200e9;
        this.DENSITY = 7800;
        this.GRAVITY = 9.81;
        
        this.L_total = 0.3;
        this.width = 0.025;
        this.thickness = 0.001;
        this.d_overhang = 0.15;
        
        this.table_width = 0.15;
        this.num_segments = 50;
        this.dx = this.L_total / this.num_segments;
        this.dt = 0.001;
        
        // 动力学变量
        this.pivot_angle = 0.0;
        this.angular_velocity = 0.0;
        this.angular_acceleration = 0.0;
        this.simulation_time = 0;
        this.damping_ratio = 0.02;
        
        // Three.js 场景对象
        this.scene = null;
        this.camera = null;
        this.renderer = null;
        this.ruler_segments = [];
        this.table = null;
        this.pivot = null;
        
        // 图表
        this.waveChart = null;
        this.soundChart = null;
        
        this.init();
    }
    
    init() {
        this.initThreeJS();
        this.initCharts();
        this.updatePhysicalParameters();
        this.applyInitialCondition(1.5);
        this.animate();
    }
    
    initThreeJS() {
        // 创建Three.js场景
        const container = document.getElementById('simulationContainer');
        this.scene = new THREE.Scene();
        this.scene.background = new THREE.Color(0xf0f0f0);
        
        // 相机设置
        this.camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 0.1, 1000);
        this.camera.position.set(0, 0.02, 0.15);
        this.camera.lookAt(0, 0, 0);
        
        // 渲染器
        this.renderer = new THREE.WebGLRenderer({ antialias: true });
        this.renderer.setSize(container.clientWidth, container.clientHeight);
        container.appendChild(this.renderer.domElement);
        
        // 灯光
        const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
        this.scene.add(ambientLight);
        
        const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
        directionalLight.position.set(10, 10, 5);
        this.scene.add(directionalLight);
        
        // 创建桌面
        const tableGeometry = new THREE.BoxGeometry(this.table_width, 0.02, this.width * 1.2);
        const tableMaterial = new THREE.MeshPhongMaterial({ color: 0x00ff00 });
        this.table = new THREE.Mesh(tableGeometry, tableMaterial);
        this.table.position.set(0, -0.01 - this.thickness/2, 0);
        this.scene.add(this.table);
        
        // 创建支点
        const pivotGeometry = new THREE.CylinderGeometry(0.002, 0.002, 0.01, 8);
        const pivotMaterial = new THREE.MeshPhongMaterial({ color: 0xffff00 });
        this.pivot = new THREE.Mesh(pivotGeometry, pivotMaterial);
        this.pivot.position.set(this.table_width/2, -0.005, 0);
        this.pivot.rotation.x = Math.PI / 2;
        this.scene.add(this.pivot);
        
        // 创建尺子分段
        this.ruler_start_x = this.table_width/2 - (this.L_total - this.d_overhang);
        
        for (let i = 0; i < this.num_segments; i++) {
            const x_pos = this.ruler_start_x + (i + 0.5) * this.dx;
            const segmentGeometry = new THREE.BoxGeometry(this.dx, this.thickness, this.width);
            const segmentMaterial = new THREE.MeshPhongMaterial({ 
                color: x_pos >= this.table_width/2 ? 0x0000ff : 0xff0000 
            });
            
            const segment = new THREE.Mesh(segmentGeometry, segmentMaterial);
            segment.position.set(x_pos, 0, 0);
            this.scene.add(segment);
            this.ruler_segments.push(segment);
        }
        
        // 窗口大小调整
        window.addEventListener('resize', () => this.onWindowResize());
    }
    
    initCharts() {
        // 初始化波形图
        const waveCtx = document.getElementById('waveChart').getContext('2d');
        this.waveChart = new Chart(waveCtx, {
            type: 'line',
            data: {
                labels: [],
                datasets: [{
                    label: '振幅 (m)',
                    borderColor: 'red',
                    backgroundColor: 'rgba(255,0,0,0.1)',
                    data: [],
                    tension: 0.4
                }]
            },
            options: {
                responsive: true,
                scales: {
                    x: {
                        title: { display: true, text: '距离支点位置 (m)' }
                    },
                    y: {
                        title: { display: true, text: '振幅 (m)' },
                        min: -0.03,
                        max: 0.03
                    }
                }
            }
        });
        
        // 初始化声波图
        const soundCtx = document.getElementById('soundChart').getContext('2d');
        this.soundChart = new Chart(soundCtx, {
            type: 'line',
            data: {
                labels: [],
                datasets: [{
                    label: '声波信号',
                    borderColor: 'blue',
                    backgroundColor: 'rgba(0,0,255,0.1)',
                    data: [],
                    tension: 0.4
                }]
            },
            options: {
                responsive: true,
                scales: {
                    x: {
                        title: { display: true, text: '时间 (s)' }
                    },
                    y: {
                        title: { display: true, text: '信号强度' },
                        min: -1,
                        max: 1
                    }
                }
            }
        });
    }
    
    updatePhysicalParameters() {
        const I = this.width * Math.pow(this.thickness, 3) / 12;
        this.EI = this.YOUNGS_MODULUS * I;
        this.mass_per_length = this.DENSITY * this.width * this.thickness;
        this.total_mass = this.mass_per_length * this.d_overhang;
        
        // 计算绕支点的转动惯量
        this.moment_of_inertia = (1/3) * this.mass_per_length * Math.pow(this.d_overhang, 3);
        
        // 悬臂梁的等效扭转刚度
        this.stiffness_coeff = (3 * this.EI) / this.d_overhang;
        
        // 阻尼系数
        this.damping_coeff = this.damping_ratio * 2 * Math.sqrt(this.stiffness_coeff * this.moment_of_inertia);
    }
    
    applyInitialCondition(force_level = 1.0) {
        this.pivot_angle = 0.1 * force_level;
        this.angular_velocity = -0.8 * force_level;
    }
    
    updateRulerGeometry() {
        this.ruler_start_x = this.table_width/2 - (this.L_total - this.d_overhang);
        
        for (let i = 0; i < this.num_segments; i++) {
            const x_pos = this.ruler_start_x + (i + 0.5) * this.dx;
            const segment = this.ruler_segments[i];
            segment.position.x = x_pos;
            
            // 更新颜色
            segment.material.color.setHex(x_pos >= this.table_width/2 ? 0x0000ff : 0xff0000);
        }
    }
    
    updatePhysics() {
        // 重力矩
        const center_of_mass_distance = this.d_overhang / 2;
        const gravity_torque = -this.total_mass * this.GRAVITY * center_of_mass_distance * Math.cos(this.pivot_angle);
        
        // 弹性恢复力矩
        const elastic_torque = -this.stiffness_coeff * this.pivot_angle;
        
        // 阻尼力矩
        const damping_torque = -this.damping_coeff * this.angular_velocity;
        
        // 总力矩
        const total_torque = gravity_torque + elastic_torque + damping_torque;
        
        // 角加速度
        this.angular_acceleration = total_torque / this.moment_of_inertia;
        
        // 更新角速度和角度
        this.angular_velocity += this.angular_acceleration * this.dt;
        this.pivot_angle += this.angular_velocity * this.dt;
        
        this.simulation_time += this.dt;
    }
    
    updateVisualization() {
        let max_deflection = 0;
        
        for (let i = 0; i < this.num_segments; i++) {
            const x_pos = this.ruler_start_x + (i + 0.5) * this.dx;
            const segment = this.ruler_segments[i];
            
            if (x_pos >= this.table_width/2 && (x_pos - this.table_width/2) <= this.d_overhang) {
                const distance = x_pos - this.table_width/2;
                
                // 主要运动：刚性旋转
                const deflection = distance * Math.sin(this.pivot_angle);
                
                // 轻微的弯曲效果
                const bending_effect = 0.02 * Math.pow(distance/this.d_overhang, 2) * Math.sin(this.pivot_angle);
                const total_deflection = deflection + bending_effect;
                
                segment.position.y = total_deflection;
                
                // 设置旋转
                const segment_angle = this.pivot_angle * 0.3;
                segment.rotation.z = segment_angle;
                
                max_deflection = Math.max(max_deflection, Math.abs(total_deflection));
            } else {
                // 桌面上的部分保持固定
                segment.position.y = 0;
                segment.rotation.z = 0;
            }
        }
        
        return max_deflection;
    }
    
    updateWaveGraph() {
        const positions = [];
        const deflections = [];
        
        for (let i = 0; i < this.num_segments; i++) {
            const x_pos = this.ruler_start_x + (i + 0.5) * this.dx;
            if (x_pos >= this.table_width/2 && (x_pos - this.table_width/2) <= this.d_overhang) {
                const distance = x_pos - this.table_width/2;
                const rigid_deflection = distance * Math.sin(this.pivot_angle);
                const bending_deflection = 0.1 * Math.pow(distance/this.d_overhang, 2) * Math.sin(this.pivot_angle);
                const total_deflection = rigid_deflection + bending_deflection;
                
                positions.push(distance);
                deflections.push(total_deflection);
            }
        }
        
        // 更新图表数据
        this.waveChart.data.labels = positions;
        this.waveChart.data.datasets[0].data = deflections;
        this.waveChart.update('none');
    }
    
    analyzeSoundWave(angle, time) {
        // 悬臂梁的基频公式
        const base_freq = Math.pow(1.875, 2) / (2 * Math.PI * Math.pow(this.d_overhang, 2)) * 
                         Math.sqrt(this.EI / this.mass_per_length);
        
        // 频率随振幅的轻微变化
        const freq_modulation = 1.0 + 0.02 * Math.abs(angle);
        const current_freq = base_freq * freq_modulation;
        
        // 声波信号
        const sound_signal = Math.sin(2 * Math.PI * current_freq * time) * Math.abs(angle) * 10;
        
        return { sound_signal, base_freq, current_freq };
    }
    
    updateSoundGraph(time, angle) {
        const { sound_signal, current_freq } = this.analyzeSoundWave(angle, time);
        
        // 更新声波图表
        if (this.soundChart.data.labels.length > 50) {
            this.soundChart.data.labels.shift();
            this.soundChart.data.datasets[0].data.shift();
        }
        
        this.soundChart.data.labels.push(time.toFixed(1));
        this.soundChart.data.datasets[0].data.push(sound_signal);
        this.soundChart.update('none');
        
        return current_freq;
    }
    
    updateRealTimeData(max_deflection, current_freq) {
        const angle_deg = this.pivot_angle * 180 / Math.PI;
        const dataElement = document.getElementById('realTimeData');
        
        dataElement.innerHTML = `
            时间: ${this.simulation_time.toFixed(1)}s<br>
            角度: ${angle_deg.toFixed(1)}°<br>
            振幅: ${(max_deflection * 1000).toFixed(1)}mm<br>
            频率: ${current_freq.toFixed(1)}Hz<br>
            状态: 振动中...
        `;
    }
    
    animate() {
        requestAnimationFrame(() => this.animate());
        
        this.updatePhysics();
        const max_deflection = this.updateVisualization();
        const current_freq = this.updateSoundGraph(this.simulation_time, this.pivot_angle);
        
        // 每0.1秒更新波形图
        if (Math.floor(this.simulation_time * 10) % 2 === 0) {
            this.updateWaveGraph();
        }
        
        this.updateRealTimeData(max_deflection, current_freq);
        this.renderer.render(this.scene, this.camera);
    }
    
    onWindowResize() {
        const container = document.getElementById('simulationContainer');
        this.camera.aspect = container.clientWidth / container.clientHeight;
        this.camera.updateProjectionMatrix();
        this.renderer.setSize(container.clientWidth, container.clientHeight);
    }
    
    // 外部控制方法
    setOverhang(value) {
        this.d_overhang = value / 1000; // 转换为米
        this.updateRulerGeometry();
        this.updatePhysicalParameters();
    }
    
    setYoungsModulus(value) {
        this.YOUNGS_MODULUS = value * 1e9;
        this.updatePhysicalParameters();
    }
    
    setThickness(value) {
        this.thickness = value / 1000; // 转换为米
        this.updateRulerGeometry();
        this.updatePhysicalParameters();
    }
    
    setDamping(value) {
        this.damping_ratio = value / 1000;
        this.updatePhysicalParameters();
    }
    
    reset() {
        this.pivot_angle = 0;
        this.angular_velocity = 0;
        this.simulation_time = 0;
        this.applyInitialCondition(1.5);
        
        // 清空图表数据
        this.waveChart.data.labels = [];
        this.waveChart.data.datasets[0].data = [];
        this.soundChart.data.labels = [];
        this.soundChart.data.datasets[0].data = [];
    }
    
    applyImpulse() {
        this.angular_velocity -= 1.0;
    }
}

// ==================== 全局模拟实例 ====================
let simulation;

// ==================== 页面加载完成后初始化 ====================
document.addEventListener('DOMContentLoaded', function() {
    simulation = new SingRulerSimulation();
    
    // 绑定控制事件
    document.getElementById('overhangSlider').oninput = function() {
        const value = this.value;
        document.getElementById('overhangValue').textContent = value + ' mm';
        simulation.setOverhang(parseFloat(value));
    };
    
    document.getElementById('youngsSlider').oninput = function() {
        const value = this.value;
        document.getElementById('youngsValue').textContent = value + ' GPa';
        simulation.setYoungsModulus(parseFloat(value));
    };
    
    document.getElementById('thicknessSlider').oninput = function() {
        const value = this.value / 100;
        document.getElementById('thicknessValue').textContent = value.toFixed(2) + ' mm';
        simulation.setThickness(parseFloat(this.value));
    };
    
    document.getElementById('dampingSlider').oninput = function() {
        const value = this.value / 1000;
        document.getElementById('dampingValue').textContent = value.toFixed(3);
        simulation.setDamping(parseFloat(this.value));
    };
});

// ==================== 控制函数 ====================
function applyImpulse() {
    if (simulation) simulation.applyImpulse();
}

function resetSimulation() {
    if (simulation) simulation.reset();
}
           </script>
</body>
</html>